package com.bdqn.util;

import com.bdqn.base.Global;
import com.bdqn.base.jedis.RedisClient;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import redis.clients.jedis.Jedis;

import java.util.*;

/**
 * JWT工具类
 *
 * @author LILIBO
 * @since 2021-09-30
 */
public class JWTUtil {

    /**
     * 获得Redis操作对象
     */
    private static Jedis jedis = RedisClient.getInstance().jedis();

    /**
     * 生成JWT（用户登录成功后）
     *
     * 使用HS256算法，生成签名的时候使用的秘钥secret,这个方法本地封装了的，一般可以从本地配置文件中读取，切记这个秘钥不能外露。它就是你服务端的私钥，在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发JWT了
     *
     * @param ttlMillis JWT过期时间
     * @param params 登录成功的账号信息，key为：subject(主体),account(账号),password(密码)
     * @return
     */
    public static String createJWT(int ttlMillis, Map<String, Object> params) {
        // 下面就是在为Payload添加各种标准声明和私有声明了，这里其实就是new一个JwtBuilder，设置JWT的body
        // setId()设置JWT的唯一标识，可以设置为一个不重复的值，主要用来作为一次性Token，从而回避重放攻击，setIssuedAt()设置创建时间，setSubject()设置主体，signWith()设置使用的签名算法和签名使用的秘钥
        JwtBuilder builder = Jwts.builder().setClaims(params).setId(UUID.randomUUID().toString()).setIssuedAt(new Date(System.currentTimeMillis())).setSubject(Global.BEARER).signWith(SignatureAlgorithm.HS256, Base64.getEncoder().encode(Global.SECRET_KEY.getBytes()));
        // 生成Token
        String token = builder.compact();
        // 存放于Redis中的Key
        String redisKey = Global.KEY_USER + Global.CACHE_POINT + params.get(Global.JWT_SUBJECT) + Global.CACHE_POINT + Global.KEY_TOKEN;
        // Token存放Redis中，并设置失效时间
        jedis.setex(redisKey, ttlMillis, token);
        return token;
    }

    /**
     * 解析JWT
     *
     * @param token 加密后的Token
     * @return
     */
    public static Claims parseJWT(String token) {
        // 使用创建JWT时相同的密钥，得到DefaultJwtParser，设置签名的秘钥，设置需要解析的JWT，最后获得Body返回
        return Jwts.parser().setSigningKey(Base64.getEncoder().encode(Global.SECRET_KEY.getBytes())).parseClaimsJws(token).getBody();
    }

    /**
     * 验证JWT
     *
     * 完整的参考下：
     * 1.将传过来的Token解析，解析出来用户Id，用户账号，用户密码。
     * 2.比较解析出来的用户密码是否跟传过来的密码一致
     * 3.如果不一致，提示：Token错误
     * 4.否则，验证Token是否失效
     * 5.如果失效，提示：Token已失效
     * 6.否则，进行下一步业务操作
     */
    public static boolean verifyJWT(String token, String account, String password) {
        try {
            // 得到DefaultJwtParser，设置签名的秘钥，设置需要解析的JWT
            Claims claims = Jwts.parser().setSigningKey(Base64.getEncoder().encode(Global.SECRET_KEY.getBytes())).parseClaimsJws(token).getBody();
            // 验证JWT签发的主体是否匹配
            if (!Global.BEARER.equals(claims.getSubject())) {
                return false;
            }
            String jwtSubject = (String) claims.get(Global.JWT_SUBJECT); // 用户ID
            String jwtAccount = (String) claims.get(Global.JWT_ACCOUNT); // 用户账号
            String jwtPassword = (String) claims.get(Global.JWT_PASSWORD); // 用户密码
            // 验证账号和密码是否匹配
            if (!jwtAccount.equals(account) || !jwtPassword.equals(password)) {
                return false;
            }
            // 验证Redis中是否存在该Key
            String redisKey = Global.KEY_USER + Global.CACHE_POINT + jwtSubject + Global.CACHE_POINT + Global.KEY_TOKEN;
            long expire = jedis.ttl(redisKey);
            System.out.println("[Redis ttl ] <" + redisKey + "> --> " + expire);
            // 如果返回-2，说明Key不存在
            if (expire == -2) {
                System.out.println("[Redis ttl ] <" + redisKey + "> --> " + expire + " (not exist)");
                return false;
            }
            // 如果返回-1，说明Key已经过了有效期
            if (expire == -1) {
                System.out.println("[Redis ttl ] <" + redisKey + "> --> " + expire + " (expire)");
                return false;
            }
            // 如果失效时间小于30分钟，将重新给Key添加失效时间（简称：续命）
            if (expire <= 3) {
                int ttlMillis = 10;
                System.out.println("[Redis ttl ] <" + redisKey + "> --> ... " + ttlMillis + "(s)");
                jedis.setex(redisKey, ttlMillis, token);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 测试创建JWT
     *
     * @param subject
     * @param account
     * @param password
     * @return
     */
    private static String testCreateJWT(String subject, String account, String password) {
        // 创建JWT
        Map<String, Object> params = new HashMap<>();
        params.put("subject", subject);
        params.put("account", account);
        params.put("password", password);
        String token = JWTUtil.createJWT(10, params);
        System.out.println("[JWT create] --> " + token);
        return token;
    }

    /**
     * 测试验证JWT
     *
     * @param token
     * @param account
     * @param password
     * @return
     */
    private static boolean testVerifyJWT(String token, String account, String password) {
        // 验证JWT
        boolean result = JWTUtil.verifyJWT(token, account, password);
        System.out.println("[JWT verify] --> " + result);
        return result;
    }

    /**
     * 测试解析JWT
     *
     * @param token
     * @return
     */
    private static boolean testParseJWT(String token) {
        // 解析JWT
        Claims claims = JWTUtil.parseJWT(token);
        String uSubject = (String) claims.get("subject");
        String uAccount = (String) claims.get("account");
        String uPassword = (String) claims.get("password");
        System.out.println("[JWT parse ] --> subject = " + uSubject);
        System.out.println("[JWT parse ] --> account = " + uAccount);
        System.out.println("[JWT parse ] --> password = " + uPassword);
        return true;
    }

    public static void main(String[] args) {
        String subject = "1"; // 用户账号（ID）
        String account = "root"; // 用户名
        String password = "LILIBO"; // 临时密码

        // 创建JWT
        String token = testCreateJWT(subject, account, password);

        try {
            for (int i = 0; i < 30; i++) {
                // 验证JWT
                Thread.sleep(1 * 1000);
                testVerifyJWT(token, account, password);
                if (i == 25) {
                    break;
                }
            }
            Thread.sleep(6 * 1000); // 等到超时
            testVerifyJWT(token, account, password);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 验证JWT
        // String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJCb0JvIiwicGFzc3dvcmQiOiJMSUxJQk8iLCJzdWJqZWN0IjoiMSIsImlhdCI6MTYzMzA1NTgxNiwiYWNjb3VudCI6InJvb3QiLCJqdGkiOiJlMDg3NjlkMy1mYzIxLTQ2OGMtOWVjNi05NDljMGE1NjM5YTQifQ.hkyG_jOTf6ageKnM2L60tHTt94fLKjdqgrZQ9PFtYnI";
        // testVerifyJWT(token, account, password);

        // 解析JWT
        // token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJCb0JvIiwicGFzc3dvcmQiOiJMSUxJQk8iLCJzdWJqZWN0IjoiMSIsImlhdCI6MTYzMzAyNTg4OCwiYWNjb3VudCI6InJvb3QiLCJqdGkiOiJiM2EyN2ZiZC03YTYxLTQ1YjgtODczOC1kNmM4MTBhOTBhOGMifQ.jg6chO_NZmKxfH4k8CbXS4x5XMbIbnKcAsq3DHO0EZs";
        // testParseJWT(token);
    }
}